import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

/**
 * @Author : lizhifeng
 * @create 2022/8/12 11:39
 * @DESC 2022/8/12 11:39
 */


public class Pool {


    private List<Thread> threadList = new ArrayList<>();

    private Pool() {
    }

    private static final Pool pool = new Pool();

    public static Pool getInstance() {
        return pool;
    }

    private Map<TransactionId, Transaction> cache = new HashMap<>();

    private Wallet wallet;

    private NetWork netWork;

    public NetWork getNetWork() {
        return netWork;
    }

    public void setNetWork(NetWork netWork) {
        this.netWork = netWork;
    }

    public Wallet getWallet() {
        return wallet;
    }

    public void setWallet(Wallet wallet) {
        this.wallet = wallet;
    }

    public synchronized void add(Transaction transaction) {
        cache.put(new TransactionId(transaction.getHash()), transaction);
    }


    public synchronized void tryCleanCache(Block latest) {
        try {
            for (Thread thread : threadList) {
                thread.interrupt();
            }
        } catch (Exception e) {

        }

        Map<TransactionId, Transaction> map = latest.getMap();
        for (TransactionId transactionId : map.keySet()) {
            cache.remove(transactionId);
        }

        for (TransactionId transactionId : latest.getTransactionIdList()) {
            cache.remove(transactionId);
        }
    }

    /**
     * 投机挖矿，不打包任何交易
     *
     * @throws Exception
     */

    public synchronized void trySpeculate() throws Exception {

        int ntime = (int) (System.currentTimeMillis() / 1000);
        int nfee = 0;

        String ptz = "packed by" + wallet.getAddress() + System.currentTimeMillis();
        Transaction transaction = createCoinBaseTransaction(ptz, wallet.getAddress(), nfee);

        List<TransactionId> transactionIdList = new ArrayList<>();
        transactionIdList.add(new TransactionId(transaction.getHash()));

        Header last = HeaderList.getInstance().getLast();
        Task task = new Task(last, netWork, transactionIdList, ntime);

        Thread thread = new Thread(task);
        thread.start();
        threadList.add(thread);
    }


    /**
     * 从交易池中随机选取若干交易尝试进行打包
     *
     * @throws Exception
     */

    public void tryPackNext() throws Exception {

        if (wallet == null) {
            System.out.println("请先创建钱包,并且给pool设置挖矿收款的钱包。");
            return;
        }

        String ptz = "packed by" + wallet.getAddress() + System.currentTimeMillis();


        Header last = HeaderList.getInstance().getLast();

        Set<Integer> set = new HashSet<>();
        List<Map.Entry<TransactionId, Transaction>> entryList = new ArrayList<>(cache.entrySet());

        int size = cache.size();
        if (cache.size() > 0) {
            for (int i = 0; i < 100; i++) {
                set.add(new Random().nextInt(size));
            }
        }

        int ntime = (int) (System.currentTimeMillis() / 1000);
        int nfee = 0;

        Transaction transaction = createCoinBaseTransaction(ptz, wallet.getAddress(), nfee);


        List<TransactionId> transactionIdList = new ArrayList<>();
        transactionIdList.add(new TransactionId(transaction.getHash()));

        for (Integer item : set) {
            Map.Entry<TransactionId, Transaction> entry = entryList.get(item);
            List<Input> preTransactions = entry.getValue().getInputs();
            if (preTransactions == null || preTransactions.size() == 0) {
                continue;
            }
            transactionIdList.add(entry.getKey());
        }


        Task task = new Task(last, netWork, transactionIdList, ntime);
        task.run();

/*        Thread thread = new Thread();
        thread.start();
        threadList.add(thread);*/
    }


    private Transaction createCoinBaseTransaction(String ptz, String address, int nfee) throws Exception {

        int nSubsidy = HeaderList.getSubsidy();

        Transaction transaction = new Transaction();
        transaction.setInputs(null);
        transaction.setPtz(ptz.getBytes());
        transaction.setCosts(Collections.singletonList(new Cost(address, nSubsidy + nfee)));

        byte[] ser = transaction.serialize(wallet);
        byte[] hash = Hash256.hash(ser);
        transaction.setHash(hash);

        netWork.broadCastTransaction(ser);
        return transaction;
    }


    public Transaction getTransaction(TransactionId transactionId) {
        return cache.get(transactionId);
    }


    private static class Task implements Runnable {

        private Header latest;
        private NetWork netWork;
        private List<TransactionId> transactionIdList;
        private int ntime;

        public Task(Header latest, NetWork netWork, List<TransactionId> transactionIdList, int ntime) {
            this.latest = latest;
            this.netWork = netWork;
            this.transactionIdList = transactionIdList;
            this.ntime = ntime;
        }

        @Override
        public void run() {

            int nbit = latest.getBits();
            int height = latest.getHeight();
            int averager = HeaderList.getInstance().average();

            int nextNbit = nbit;
            if (averager > 10) {
                nextNbit = nbit - 1;
            } else if (averager < 10) {
                nextNbit = nbit + 1;
            }

            if (nextNbit < Constant.MIN_BITS) {
                nextNbit = Constant.MIN_BITS;
            }

            System.out.println(Thread.currentThread() + ":average=" + averager + ":nextNbits=" + nextNbit);

            byte[] preHash = Hash256.hash(Hash256.hash(latest.serialize()));
            byte[] merkleRoot = Miner.buildMerkleRoot(transactionIdList);

            try {
                Header header = new Header();
                header.setBits(nextNbit);
                header.setHeight(height + 1);
                header.setPreHash(preHash);
                header.setTime(ntime);
                header.setVersion(1);
                header.setMerkleRoot(merkleRoot);

                boolean result = Miner.mining(header);
                if (result) {
                    Block block = new Block();
                    block.setHeader(header);
                    block.setTransactionIdList(transactionIdList);
                    netWork.broadCastBlock(block);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }


}
